package com.misc.tunewsreader;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Application;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.preference.PreferenceManager;
import android.util.Base64;
import android.util.Log;

public class NNTPClient {
	private static String TAG = "NNTP";

	private static Socket socket;
	private static DataOutputStream dataOut;
	private static BufferedReader dataIn;
	private static PrintWriter out;
	private static boolean connected;
	private static NNTPGroup current_group = null;
	private static boolean is_offline = false;
	private static String nntp_host = null;
	private static int nntp_port = 0;
	private static boolean is_stopped = false;
	private static boolean is_loading = false;
	private static int load_limit = -1;

	private static SQLiteDB db;
	private static Context context;

	private static List<NNTPArticleHeader> post_headers;
	private static final SimpleDateFormat date_format = new SimpleDateFormat(
			"EEE, d MMM yyyy HH:mm:ss Z", Locale.ENGLISH);
	private static final DateFormat new_date_format = DateFormat
			.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
	private static final SimpleDateFormat sortable_date_format = new SimpleDateFormat(
			"yyyy-MM-dd-HH-mm-ss");
	private static HashMap<String, Integer> thread_replies = null;
	private static HashMap<String, Integer> thread_replies_unread = null;

	private static String user_agent = "TU Graz Newsreader/Android "
			+ android.os.Build.VERSION.RELEASE;

	/**
	 * Queries whether the phone has an active internet connection or not
	 * 
	 * @return true if internet connection is available, false otherwise
	 */
	public static boolean isOnline() {
		ConnectivityManager connectivity = (ConnectivityManager) context
				.getSystemService(Context.CONNECTIVITY_SERVICE);

		if (connectivity == null
				|| connectivity.getActiveNetworkInfo() == null
				|| !connectivity.getActiveNetworkInfo()
						.isConnectedOrConnecting()) {
			return false;
		}
		return true;
	}

	/**
	 * Connect to the NNTP server specified by host and port.
	 * 
	 * @param ctx
	 *            context of the application
	 * @param host
	 *            NNTP server host
	 * @param port
	 *            NNTP server port (usually 119)
	 */
	public static void connect(Context ctx, String host, int port) {
		context = ctx;
		nntp_host = host;
		nntp_port = port;
		reconnect();
	}

	/**
	 * Reconnect to the NNTP server
	 */
	public static void reconnect() {
		if (nntp_host == null)
			return;

		connected = false;
		is_offline = true;

		if (isOnline()) {
			try {
				socket = new Socket(nntp_host, nntp_port);
				dataOut = new DataOutputStream(socket.getOutputStream());
				// dataIn = new DataInputStream(socket.getInputStream());
				dataIn = new BufferedReader(new InputStreamReader(
						socket.getInputStream(), "UTF-8"));

				out = new PrintWriter(new BufferedWriter(
						new OutputStreamWriter(dataOut)));

				String line = getOneLineReply();

				if (line.startsWith("200")) {
					connected = true;
					is_offline = false;
				} else {
					connected = false;
					is_offline = true;
				}
			} catch (UnknownHostException e) {
				Log.d(TAG, "Unknown host \"" + nntp_host + "\"");
			} catch (IOException e) {
				Log.d(TAG, "IO Exception while trying to connect");
			}
		}

		db = new SQLiteDB(context, "posts");
		db.open();
	}

	/**
	 * Switches to offline mode (and drops connection if online)
	 * 
	 * @param offline
	 *            true to switch to offline mode and drop connection, false to
	 *            switch to online mode
	 */
	public static void setOfflineMode(boolean offline) {
		if (offline)
			connected = false;
		is_offline = offline;
	}

	/**
	 * Returns whether the newsreader is in offline mode
	 * 
	 * @return true if in offline mode, false otherwise
	 */
	public static boolean isOfflineMode() {
		return is_offline;
	}

	/**
	 * Send a NNTP command to the server
	 * 
	 * @param cmd
	 *            NNTP command (e.g. article, post, ...)
	 */
	private synchronized static void sendCommand(String cmd) {
		if (!connected)
			return;
		out.println(cmd);
		out.flush();
	}

	/**
	 * Get the answer of a NNTP command.
	 * 
	 * @return a list of result lines
	 */
	private synchronized static List<String> getReply() {
		if (!connected)
			return null;

		List<String> res = new ArrayList<String>();
		String line = "";
		do {
			try {
				line = dataIn.readLine();
				if (line == null) {
					connected = false;
					is_offline = true;
					break;
				}
				res.add(line);
			} catch (IOException e) {
				connected = false;
				is_offline = true;
				e.printStackTrace();
			}
			if (line != null && line.trim().equals(".") && line.startsWith(".")) {
				break;
			}
		} while (true);
		return res;
	}

	/**
	 * Get one reply line of a NNTP command. Used to get the status code
	 * 
	 * @return the first line of the NNTP reply
	 */
	private synchronized static String getOneLineReply() {
		String line = "";
		try {
			line = dataIn.readLine();
			Log.d(TAG, "Received line: " + line);
		} catch (Exception e) {
			connected = false;
			is_offline = true;
			e.printStackTrace();
		}
		return line; // line.toString();
	}

	/**
	 * Request the list of all available newsgroups on the server
	 * 
	 * @return list of all newsgroups
	 */
	public static List<NNTPGroup> getGroups() {
		if (!connected)
			return null;

		sendCommand("list");

		List<String> res = getReply();
		List<NNTPGroup> groups = new ArrayList<NNTPGroup>();
		for (int i = 0; i < res.size(); i++) {
			if (res.get(i).startsWith("215"))
				continue;
			if (res.get(i).trim().equals("."))
				continue;
			groups.add(new NNTPGroup(res.get(i)));
		}
		return groups;
	}

	/**
	 * Download group info from server
	 * 
	 * @param name
	 *            Name of the newsgroup
	 * @return group
	 */
	public static NNTPGroup getGroupInfo(String name) {
		if (is_offline || !connected)
			return null;
		sendCommand("group " + name);
		NNTPGroup group = new NNTPGroup(null);
		group.enter(getOneLineReply());
		return group;
	}

	/**
	 * Enter the given newsgroup and set it as currently active
	 * 
	 * @param group
	 *            newsgroup to enter
	 */
	public static void enterGroup(NNTPGroup group) {
		if (is_offline) {
			enterGroupOffline(group);
			return;
		}

		if (!connected)
			return;
		sendCommand("group " + group.getName());
		group.enter(getOneLineReply());
		current_group = group;
	}

	/**
	 * Enter the given newsgroup and set it as currently active when in offline
	 * mode.
	 * 
	 * @deprecated This function should not be used, use @see enterGroup instead
	 * 
	 * @param group
	 */
	@Deprecated
	public static void enterGroupOffline(NNTPGroup group) {
		current_group = group;
		is_offline = true;
	}

	/**
	 * Save all group details of the given newsgroup to the database
	 * 
	 * @param group
	 *            Newsgroup
	 */
	public static void saveGroup(NNTPGroup group) {
		db.saveGroupInfo(group);
	}

	/**
	 * Delete all data associated to the given newsgroup from the database (this
	 * includes group info and all posts)
	 * 
	 * @param group
	 *            Newsgroup
	 */
	public static void resetGroup(NNTPGroup group) {
		db.deleteGroupInfo(group);
		db.deleteGroupPosts(group);
	}

	public static void cancelPost(String newsgroup, NNTPPost post) {
		db.deletePost(post.getHeader().getThreadId(), newsgroup);
	}

	/**
	 * Delete old posts from the newsgroup, keeping only a given number of posts
	 * in the database
	 * 
	 * @param group
	 *            Newsgroup
	 * @param leave_posts
	 *            Number of posts to keep in database
	 */
	public static void cleanupGroup(NNTPGroup group, int leave_posts) {
		List<NNTPArticleHeader> posts = db.getPostHeaders(group.getName());
		int to_delete = posts.size() - leave_posts;
		for (NNTPArticleHeader p : posts) {
			if (to_delete > 0) {
				db.deletePost(p.getThreadId(), group.getName());
				to_delete--;
			} else {
				break;
			}
		}
	}

	/**
	 * Deletes the group info from the database and forces a refresh of the
	 * group
	 * 
	 * @param group
	 *            Newsgroup
	 */
	public static void reloadGroup(NNTPGroup group) {
		db.deleteGroupInfo(group);
	}

	/**
	 * Get the newsgroup that is currently active
	 * 
	 * @return currently active newsgroup
	 */
	public static NNTPGroup getCurrentGroup() {
		if (is_offline) {
			return getCurrentGroupOffline();
		}

		if (!connected)
			return null;
		return current_group;
	}

	/**
	 * Get the newsgroup that is currently active in offline mode
	 * 
	 * @deprecated use @see getCurrentGroup instead
	 * @return currently active newsgroups
	 */
	@Deprecated
	public static NNTPGroup getCurrentGroupOffline() {
		is_offline = true;
		return current_group;
	}

	/**
	 * Get the number of unread posts that are already downloaded for a
	 * newsgroup
	 * 
	 * @param group
	 *            Newsgroup
	 * @return number of unread posts
	 */
	public static int getUnreadCount(String group) {
		return db.getUnreadCount(group);
	}

	/**
	 * Return the number of unread posts plus the number of unread posts not yet
	 * downloaded. This number is not guaranteed to be exact!
	 * 
	 * @param group
	 *            Newsgroup name
	 * @param info
	 *            Newsgroup
	 * @return number of new plus unread posts
	 */
	public static int getNewAndUnreadCount(String group, NNTPGroup info) {
		int unread = db.getUnreadCount(group);
		NNTPGroup saved_info = db.getGroupInfo(group);
		if (saved_info == null)
			return -1;
		Log.d("NNTP", "G: " + info.getName() + ", U: " + unread + ", GLast: "
				+ info.getFirstId() + " - " + info.getLastId() + ", SLast: "
				+ saved_info.getLastId());
		if (info.getFirstId() == saved_info.getFirstId())
			unread += (info.getLastId() - saved_info.getLastId());
		return unread;
	}

	/**
	 * Return the number of new not downloaded posts on the server for the given
	 * newsgroup
	 * 
	 * @param group
	 *            name of the group
	 * @return number of posts (0 on error)
	 */
	public static int getNewPostsOnServer(String group) {
		NNTPGroup saved_info = db.getGroupInfo(group);
		if (saved_info == null)
			return 0;
		NNTPGroup info = getGroupInfo(group);
		if (info == null)
			return 0;
		Log.d("NNTP",
				"Saved: " + saved_info.getFirstId() + " - "
						+ saved_info.getLastId() + ", New: "
						+ info.getFirstId() + " - " + info.getLastId());
		if (info.getFirstId() == saved_info.getFirstId())
			return (int) (info.getLastId() - saved_info.getLastId());
		return 0;
	}

	/**
	 * Downloads the raw (not converted or parsed) header of a message from the
	 * server
	 * 
	 * @param message_id
	 *            the message ID (in form <...@...>)
	 * @return list of lines of the post header
	 */
	public static List<String> loadRawPostHeader(String message_id) {
		List<String> ret;
		// fetch article
		sendCommand("head " + message_id);
		String stat = getOneLineReply();
		// some error
		if (stat == null || !stat.startsWith("221"))
			return null;
		ret = getReply();
		return ret;
	}

	/**
	 * Returns whether the user has aborted the loading of all posts
	 * 
	 * @return true if the loading was aborted, false otherwise
	 */
	private static boolean isStopped() {
		return is_stopped;
	}

	/**
	 * Abort the loading of all posts
	 */
	public static void stopLoading() {
		is_stopped = true;
	}

	/**
	 * Check whether the NNTPClient is currently downloading all posts
	 * 
	 * @return true if the posts are being downloaded, false otherwise
	 */
	public static boolean isLoadingAllPosts() {
		return is_loading;
	}

	/**
	 * Get the number of new posts on the server for the current newsgroup
	 * 
	 * @return number of new posts
	 */
	public static int getNewPostCount() {
		NNTPGroup group = (db != null ? db
				.getGroupInfo(current_group.getName()) : null);

		long start_id = -1;
		if (group != null) {
			start_id = group.getLastId();

			if (current_group != null
					&& current_group.getFirstId() == group.getFirstId()
					&& current_group.getLastId() == group.getLastId()) {
				return 0;
			}
		}
		if (start_id == -1) {
			if (current_group != null)
				start_id = current_group.getFirstId();
			else
				start_id = 0;
		}

		long last_id = (current_group == null ? 0 : current_group.getLastId());
		return (int) (last_id - start_id);
	}

	/**
	 * Limit the maximum number of posts downloaded by @see loadAllPosts
	 * 
	 * @param limit
	 *            Maximum number of posts (-1 for unlimited)
	 */
	public static void setLoadLimit(int limit) {
		load_limit = limit;
	}

	/**
	 * Load all posts from the current newsgroup and save them in the database.
	 * Already downloaded posts are retrieved from the database. The function
	 * works in offline mode too. Use @see setLoadLimit to set the maximum
	 * number of posts to download
	 * 
	 * @param with_text
	 *            Load posts with text (should be true)
	 * @param progress
	 *            ProgessDialog object to show a progress (can be null)
	 */
	public static void loadAllPosts(boolean with_text,
			final ProgressDialog progress) {
		is_loading = true;
		NNTPGroup group = db.getGroupInfo(current_group.getName());

		long start_id = -1;
		if (group != null) {
			Log.d(TAG,
					"Saved info: " + group.getFirstId() + " - "
							+ group.getLastId());
			start_id = group.getLastId();

			if (current_group.getFirstId() == group.getFirstId()
					&& current_group.getLastId() == group.getLastId()) {
				Log.d(TAG, "No need to load data from newsgroup");
				post_headers = db.getPostHeaders(current_group.getName());
				is_loading = false;
				return;
			}

		}

		// in offline mode, we can only load from database
		if (is_offline) {
			post_headers = db.getPostHeaders(current_group.getName());
			return;
		}

		// we have to load posts from server, reset the post count for all
		// threads
		resetRepliesReadAndUnread();

		if (start_id == -1)
			start_id = current_group.getFirstId();

		// load only the last load_limit posts if set
		if (load_limit != -1) {
			start_id = Math.max(start_id, current_group.getLastId()
					- load_limit);
			load_limit = -1;
		}

		// search for last post on server
		String status;
		do {
			sendCommand("head " + start_id);
			status = getOneLineReply();
			if (status.startsWith("221")) {
				getReply();
			} else {
				start_id--;
			}
		} while (!status.startsWith("221")
				&& start_id > current_group.getFirstId());

		// initialize progress dialog
		if (progress != null) {
			progress.setIndeterminate(false);
			progress.setMax((int) (current_group.getLastId() - start_id));
			progress.setProgress(0);
			// show progress dialog
			if (context != null) {
				((Activity) context).runOnUiThread(new Runnable() {
					public void run() {
						progress.show();
					}
				});
			}
		}

		db.startTransaction();

		if (start_id == current_group.getFirstId()) {
			// post_headers = new ArrayList<NNTPArticleHeader>();
		} else {
			// post_headers = db.getPostHeaders(current_group.getName());
			sendCommand("next");
			getOneLineReply();
		}

		String stat = "";
		do {
			// check if thread is interrupted - if so, cancel loading and return
			// an empty list
			if (isStopped()) {
				is_stopped = false;
				post_headers = new ArrayList<NNTPArticleHeader>();
				db.unsuccessfulTransaction();
				is_loading = false;
				return;
			}

			if (with_text)
				sendCommand("article");
			else
				sendCommand("head");
			stat = getOneLineReply();

			// some error occured, abort
			if (stat == null) {
				is_loading = false;
				post_headers = new ArrayList<NNTPArticleHeader>();
				db.unsuccessfulTransaction();
				return;
			}

			// head only
			if (stat.startsWith("221")) {
				String[] info = stat.split(" ");

				NNTPArticleHeader header = new NNTPArticleHeader(getReply(),
						Long.parseLong(info[1]), 0);
				// post_headers.add(header);

				db.savePostHeader(current_group.getName(), header);
			}

			// head + article
			if (stat.startsWith("220")) {
				String body = "";
				StringBuilder body_b = new StringBuilder();
				// String head = "";
				StringBuilder head_b = new StringBuilder();
				List<String> data = getReply();
				List<String> head_list = new ArrayList<String>();
				boolean start = false;
				boolean quoted_printable = false;

				boolean has_attachment = false;
				String boundary = "";
				boolean attach_info = true;
				int section = 0;
				StringBuilder attachment_b = new StringBuilder();
				String attach_type = "";
				String attach_filename = "";

				for (int i = 0; i < data.size(); i++) {
					if (data.get(i).trim().length() == 0 && !start) {
						start = true;
						continue;
					}
					if (!start) {
						// head += data.get(i) + "\r\n";
						head_b.append(data.get(i) + "\r\n");
						head_list.add(data.get(i));
						// TODO: properly handle attachment
						if (data.get(i).contains(
								"Content-Type: multipart/mixed")) {
							has_attachment = true;
							if (i + 1 < data.size()) {
								String[] bound = data.get(i + 1).split(
										"boundary=");
								if (bound.length != 2)
									break;
								boundary = bound[1].replace("\"", " ").trim();
							} else
								break;
						}
						if (data.get(i).contains("Content-Transfer-Encoding:")) {
							if (data.get(i).contains("quoted-printable"))
								quoted_printable = true;
						}
					}

					if (start
							&& !(data.get(i).trim().equals(".") && data.get(i)
									.startsWith("."))) {
						// post has attachment, strip it for the body
						if (has_attachment
								&& data.get(i).trim().contains(boundary)) {
							section++;
							if (section == 1) {
								while (!data.get(i).trim().equals("")
										&& i < data.size() - 1) {
									i++;
								}
								i++;
							}
						}
						// add to post body
						if (!has_attachment || (has_attachment && section == 1))
							body_b.append(data.get(i) + "\r\n");

						// read attachment infos
						if (has_attachment && section == 2) {
							if (attach_info) {
								if (data.get(i).trim().equals(""))
									attach_info = false;
								Log.d("NNTP", "attach-info: "
										+ data.get(i).trim());
								if (data.get(i).trim().startsWith("Content-Type:")) {
									attach_type = data
											.get(i)
											.trim()
											.substring(14,
													data.get(i).length() - 1)
											.trim();
									Log.d("NNTP", "attach-content-type: "
											+ attach_type);
								} else if (data.get(i).trim().startsWith("filename=")) {
									attach_filename = data
											.get(i)
											.trim()
											.substring(9,
													data.get(i).length() - 1)
											.trim().replace("\"", "").trim();
								}
							} else {
								attachment_b.append(data.get(i).trim());
							}
						}

					}
				}

				if (has_attachment) {
					SharedPreferences prefs = PreferenceManager
							.getDefaultSharedPreferences(context);
					
					// if image, show it
					if (attach_type.contains("image") && prefs.getBoolean("pref_attached_image", true))
						body_b.append("%attach src=\"data:" + attach_type
								+ ";base64," + attachment_b.toString() + "\">");
					// otherwise show download link
					else {
						Log.d("NNTP", "attach-file: " + attach_filename);
						body_b.append("%dlattach 'data:application/octet-stream;charset=\"" + attach_filename + "\";base64,"
								+ attachment_b.toString() + "'>" + attach_filename + " %dlendattach");
					}
				}

				String[] info = stat.split(" ");

				NNTPArticleHeader header = new NNTPArticleHeader(head_list,
						Long.parseLong(info[1]), 0);

				if (!header.getCharset().toUpperCase().equals("UTF-8")) {
					Log.d(TAG, "Charset: " + header.getCharset());
					body = getBody(header.getThreadId(), header.getCharset());
				} else
					body = body_b.toString();

				// if transfer encoding is quoted printable, encode it
				if (quoted_printable) {
					try {
						body = new String(decodeQP(body), header.getCharset());
					} catch (UnsupportedEncodingException e) {
					}
				}

				// post_headers.add(header);
				db.savePostBody(current_group.getName(),
						Long.parseLong(info[1]), head_b.toString(), body);
			}
			if (progress != null) {
				progress.incrementProgressBy(1);
			}
			sendCommand("next");
		} while (getOneLineReply().startsWith("223"));

		db.saveGroupInfo(current_group);
		db.successfulTransaction();
		post_headers = db.getPostHeaders(current_group.getName());
		// change orphaned posts to threads
		if (progress != null) {
			progress.setIndeterminate(true);
		}
		boolean had_orphans = false;
		for (int i = 0; i < post_headers.size(); i++) {
			NNTPArticleHeader post = post_headers.get(i);
			// parents can't be orphaned
			if (post.isParent())
				continue;
			boolean is_orphan = true;
			for (int j = 0; j < post_headers.size(); j++) {
				if (post.get("References").endsWith(
						post_headers.get(j).getMessageID())) {
					is_orphan = false;
					break;
				}
			}
			// convert to thread
			if (is_orphan) {
				Log.d("NNTP", "Orphaned post: " + post.getSubject());
				had_orphans = true;
				db.deletePost(post.getThreadId(), current_group.getName());
				post.setParent();
				db.savePostHeader(current_group.getName(), post);
			}

		}
		// reload posts if there were orphans
		if (had_orphans) {
			post_headers = db.getPostHeaders(current_group.getName());
		}
		is_loading = false;
	}

	/**
	 * Return a sorted list of all threads in the current newsgroup
	 * 
	 * @return list of all threads
	 */
	public static List<NNTPThreadInfo> getAllThreads() {
		List<NNTPThreadInfo> titles = new ArrayList<NNTPThreadInfo>();
		Log.d(TAG, "Headers: " + post_headers.size());
		for (int i = 0; i < post_headers.size(); i++) {
			NNTPArticleHeader header = post_headers.get(i);
			if (header.isParent()) {
				titles.add(new NNTPThreadInfo(header));
			}
		}
		Collections.sort(titles, new Comparator<NNTPThreadInfo>() {
			@Override
			public int compare(NNTPThreadInfo info1, NNTPThreadInfo info2) {
				return info2.getDateSortable().compareTo(
						info1.getDateSortable());
			}
		});
		// sort from oldest to newest
		SharedPreferences prefs = PreferenceManager
				.getDefaultSharedPreferences(context);
		if (!prefs.getBoolean("pref_sort", true))
			Collections.reverse(titles);

		Log.d(TAG, "Threads: " + titles.size());
		return titles;
	}

	/**
	 * Get the first post (root post) of a thread
	 * 
	 * @param thread
	 *            thread ID
	 * @return first post of the thread
	 */
	private static NNTPArticleHeader firstArticle(NNTPThreadInfo thread) {
		if (post_headers == null || thread == null)
			return null;
		for (int i = 0; i < post_headers.size(); i++) {
			if (post_headers.get(i).getThreadId() == thread.getThreadId())
				return post_headers.get(i);
		}
		return null;
	}

	/**
	 * Loads the body of a UTF-8 formatted post
	 * 
	 * @param id
	 *            post ID
	 * @return raw post body
	 */
	public static String getBody(long id) {
		return getBody(id, "UTF-8");
	}

	/**
	 * Loads the body of a post that is encoded with the given charset
	 * 
	 * @param id
	 *            post ID
	 * @param charset
	 *            the charset used to encode the post
	 * @return raw post body
	 */
	public static String getBody(long id, String charset) {
		if (current_group == null)
			return "Some error occurred - please try again";

		String body = db.getPostBody(current_group.getName(), id);
		if (body == "") {
			String header = "";
			if (post_headers != null) {
				for (int j = 0; j < post_headers.size(); j++) {
					if (post_headers.get(j).getThreadId() == id) {
						header = post_headers.get(j).getAsString();
						break;
					}
				}
			}

			// change charset
			try {
				dataIn = new BufferedReader(new InputStreamReader(
						socket.getInputStream(), charset));
			} catch (Exception e) {
			}

			sendCommand("article " + id);
			String stat = getOneLineReply();
			if (stat.startsWith("220")) {
				List<String> data = getReply();
				boolean start = false;
				for (int i = 0; i < data.size(); i++) {
					if (data.get(i).trim().length() == 0 && !start) {
						start = true;
						continue;
					}

					if (start
							&& !(data.get(i).startsWith(".") && data.get(i)
									.trim().equals("."))) {
						body += data.get(i) + "\r\n";
					}
				}
				Log.d("NNTPSavePost", body.replaceAll("\n", "#"));
				db.savePostBody(current_group.getName(), id, header, body);
			} else if (stat.startsWith("423")) {
				body = "Error - invalid post";
			}

			// change charset back
			try {
				dataIn = new BufferedReader(new InputStreamReader(
						socket.getInputStream(), "UTF-8"));
			} catch (Exception e) {
			}

		}
		return body;
	}

	/**
	 * Get a list of all replies to a thread
	 * 
	 * @param thread
	 *            thread to get the replies for
	 * @return list of posts
	 */
	public static List<NNTPPost> getReplies(NNTPThreadInfo thread) {
		NNTPArticleHeader first = firstArticle(thread);
		if (first == null)
			return null;

		return getReplies(first, 0);
	}

	/**
	 * Get a list of all replies to a specific post, starting at a given reply
	 * level (usually 0)
	 * 
	 * @param first
	 *            post to get the replies for
	 * @param level
	 *            starting reply level (0 to get all replies)
	 * @return list of replies
	 */
	public static List<NNTPPost> getReplies(NNTPArticleHeader first, int level) {
		List<NNTPPost> replies = new ArrayList<NNTPPost>();

		String m_id = first.get("Message-ID");

		for (int i = 0; i < post_headers.size(); i++) {
			NNTPArticleHeader header = post_headers.get(i);
			if (header.get("References").endsWith(m_id)) {
				NNTPPost post = new NNTPPost();
				post.setData("");// getBody(header.getThreadId()));
				post.setHeader(header);
				post.setLayer(level);
				replies.add(post);

				// recursively add replies
				List<NNTPPost> sub = getReplies(header, level + 1);
				for (int j = 0; j < sub.size(); j++) {
					replies.add(sub.get(j));
				}
			}
		}
		return replies;
	}

	public static int getRepliesCount(String thread_id) {
		if (thread_replies == null)
			return -1;
		if (!thread_replies.containsKey(thread_id))
			return -1;
		return thread_replies.get(thread_id);
	}

	public static int getRepliesUnreadCount(String thread_id) {
		if (thread_replies_unread == null)
			return -1;
		if (!thread_replies_unread.containsKey(thread_id))
			return -1;
		return thread_replies_unread.get(thread_id);
	}

	public static void setRepliesCount(String thread_id, int count) {
		if (thread_replies == null)
			thread_replies = new HashMap<String, Integer>();
		thread_replies.put(thread_id, count);
	}

	public static void setRepliesUnreadCount(String thread_id, int count) {
		if (thread_replies_unread == null)
			thread_replies_unread = new HashMap<String, Integer>();
		thread_replies_unread.put(thread_id, count);
	}

	public static void resetRepliesReadAndUnread() {
		thread_replies_unread = null;
		thread_replies = null;
	}

	public static NNTPThreadInfo getThreadById(long id) {
		if (post_headers == null)
			return null;
		for (int i = 0; i < post_headers.size(); i++) {
			if (post_headers.get(i).getThreadId() == id)
				return new NNTPThreadInfo(post_headers.get(i));
		}
		return null;
	}

	public static NNTPPost getPostById(long id) {
		NNTPPost post = new NNTPPost();
		if (post_headers != null) {
			for (int i = 0; i < post_headers.size(); i++) {
				if (post_headers.get(i) != null
						&& post_headers.get(i).getThreadId() == id) {
					post.setHeader(post_headers.get(i));
					post.setData("");
					break;
				}
			}
		}
		return post;
	}

	public static void markPostsReadByMsgId(List<String> ids) {
		for (int i = 0; i < post_headers.size(); i++) {
			for (int j = 0; j < ids.size(); j++) {
				if (post_headers.get(i).getMessageID().trim()
						.equals(ids.get(j).trim())) {
					Log.d("Sync", "Mark " + ids.get(j) + " as read");
					markPostRead(post_headers.get(i).getThreadId(), true);
					post_headers.get(i).setRead(true);
				}
			}
		}
	}

	public static void markPostRead(long id, boolean is_read) {
		NNTPPost post = getPostById(id);
		Sync.setRead(PreferenceManager.getDefaultSharedPreferences(context)
				.getString("pref_sync_key", ""), current_group.getName(), post
				.getHeader().getMessageID(), is_read);
		db.markPostRead(current_group.getName(), id, is_read);
	}

	public static void markPostReadById(String id, boolean is_read) {
		db.markPostReadById(id, is_read);
	}

	public static void markGroupRead(String group, boolean is_read) {
		db.markGroupRead(group, is_read);
		thread_replies = null;
		thread_replies_unread = null;
	}

	/**
	 * Extract the message ID from a string (assuming the message ID starts with
	 * < and ends with >)
	 * 
	 * @param stat
	 *            the string to extract from
	 * @return message ID
	 */
	private static String extractMessageId(String stat) {
		Pattern pattern = Pattern.compile("\\<([^\\>]*)\\>");
		Matcher matcher = pattern.matcher(stat);
		if (matcher.find()) {
			return matcher.group(0).trim();
		} else
			return "";
	}

	/**
	 * Format author name and mail for newsgroup posting
	 * 
	 * @param name
	 *            author name
	 * @param mail
	 *            author mail
	 * @return formated author string
	 */
	public static String formatSender(String name, String mail) {
		return NNTPClient.encodeText(name.replaceAll("[<>\\(\\)]", "") + " <"
				+ mail + ">");
	}

	/**
	 * Post a message. All values must already be encoded correctly. There is no
	 * sanity check.
	 * 
	 * @param header
	 *            hash map of header fields
	 * @param text
	 *            the post body as string
	 * @return true if the post was posted successfully, false otherwise
	 */
	private static boolean postPost(HashMap<String, String> header, String text) {
		sendCommand("post");
		String stat = getOneLineReply();
		if (!stat.startsWith("340"))
			return false;

		// extract message id
		String m_id = extractMessageId(stat);
		if (m_id.equals(""))
			return false;

		// add message id
		header.put("Message-ID", m_id);

		// send headers
		Iterator<HashMap.Entry<String, String>> it = header.entrySet()
				.iterator();
		while (it.hasNext()) {
			HashMap.Entry<String, String> pairs = (HashMap.Entry<String, String>) it
					.next();
			// Log.d("NNTPPost", pairs.getKey() + ":" + pairs.getValue());
			sendCommand(pairs.getKey() + ": " + pairs.getValue());
		}

		// end of headers
		sendCommand("");

		// send text
		String txt_lines[] = text.split("\n");
		String txt_send = "";
		for (int i = 0; i < txt_lines.length; i++) {
			// remove \r at end
			if (txt_lines[i].endsWith("\r"))
				txt_lines[i] = txt_lines[i].substring(0,
						txt_lines[i].length() - 1);
			// mask . (dot)
			if (txt_lines[i].startsWith("."))
				txt_lines[i] = " " + txt_lines[i];
			// if only newline, add space
			if (txt_lines[i].length() == 0)
				txt_lines[i] += " ";
			txt_send += txt_lines[i] + "\r\n";
		}
		sendCommand(txt_send);

		// end of post
		sendCommand(".");

		// return false if posting failed
		stat = getOneLineReply();
		if (!stat.startsWith("240"))
			return false;

		return true;
	}

	/**
	 * Post a message to a newsgroup. The values must be encoded properly.
	 * 
	 * @param newsgroup
	 *            newsgroup name
	 * @param subject
	 *            post subject
	 * @param sender
	 *            post author (in form "username <...@...>")
	 * @param text
	 *            post message
	 * @return true if post was posted successfully, false otherwise
	 */
	public static boolean postToNewsgroup(String newsgroup, String subject,
			String sender, String text) {

		HashMap<String, String> header = new HashMap<String, String>();
		header.put("From", sender);
		header.put("Subject", subject);
		header.put("Newsgroups", newsgroup);
		header.put("Content-Type", "text/plain; charset=UTF-8; format=flowed");
		header.put("Content-Transfer-Encoding", "8bit");
		header.put("Mime-Version", "1.0");
		header.put("User-Agent", user_agent);

		return postPost(header, text);
	}

	/**
	 * Post a control message to a newsgroup. The values must be encoded
	 * properly.
	 * 
	 * @param newsgroup
	 *            newsgroup name
	 * @param subject
	 *            post subject
	 * @param sender
	 *            post author (in form "username <...@...>")
	 * @param text
	 *            post message
	 * @return true if post was posted successfully, false otherwise
	 */
	public static boolean postControlMessage(String newsgroup, String subject,
			String sender, String text) {
		HashMap<String, String> header = new HashMap<String, String>();
		header.put("From", sender);
		header.put("Subject", subject);
		header.put("Control", subject);
		header.put("Newsgroups", newsgroup);
		header.put("Content-Type", "text/plain; charset=UTF-8; format=flowed");
		header.put("Mime-Version", "1.0");
		header.put("User-Agent", user_agent);

		return postPost(header, text);
	}

	/**
	 * Post an answer to a post
	 * 
	 * @param newsgroup
	 *            newsgroup name
	 * @param reply_to
	 *            message ID of parent post
	 * @param subject
	 *            post subject
	 * @param sender
	 *            post author (in form "username <...@...>")
	 * @param text
	 *            post message
	 * @return true if reply was posted successfully, false otherwise
	 */
	public static boolean postAnswer(String newsgroup, String reply_to,
			String subject, String sender, String text) {
		HashMap<String, String> header = new HashMap<String, String>();
		header.put("From", sender);
		header.put("Subject", subject);
		header.put("Newsgroups", newsgroup);
		header.put("References", reply_to);
		header.put("Content-Type", "text/plain; charset=UTF-8; format=flowed");
		header.put("Content-Transfer-Encoding", "8bit");
		header.put("Mime-Version", "1.0");
		header.put("User-Agent", user_agent);

		return postPost(header, text);
	}

	/**
	 * Converts the NNTP date string (in form EEE, d MMM yyyy HH:mm:ss Z) to the
	 * system date format.
	 * 
	 * @param datestr
	 *            NNTP date string
	 * @return date formatted as specified by the users locale
	 */
	public static String convertDate(String datestr) {
		// format: Wed, 06 Jun 2012 03:02:55
		try {
			Date date = date_format.parse(datestr);
			return new_date_format.format(date);
		} catch (ParseException e) {
			e.printStackTrace();
		}
		return datestr;
	}

	/**
	 * Converts a NNTP date string (in form EEE, d MMM yyyy HH:mm:ss Z) to a
	 * sortable date string (in form yyyy-MM-dd-HH-mm-ss)
	 * 
	 * @param datestr
	 *            NNTP date string
	 * @return sortable date string
	 */
	public static String convertDateSortable(String datestr) {
		try {
			Date date = date_format.parse(datestr);
			return sortable_date_format.format(date);
		} catch (ParseException e) {
			e.printStackTrace();
		}
		return datestr;
	}

	/**
	 * Decodes a raw quoted printable string to an UTF-8 byte array
	 * 
	 * @param str
	 *            quoted printable encoded string
	 * @return UTF-8 byte array
	 */
	public static byte[] decodeQP(String str) {
		Log.d("QP Encode", str);
		boolean in_utf = false;
		List<Byte> utf_str = new ArrayList<Byte>();
		for (int i = 0; i < str.length(); i++) {
			if (str.charAt(i) == '=') {
				in_utf = true;
				continue;
			}
			if (!in_utf) {
				if (str.charAt(i) != '_')
					utf_str.add((byte) str.charAt(i));
				else
					utf_str.add((byte) ' ');
			} else {
				if (i < str.length() - 1) {
					String hex = str.charAt(i) + "" + str.charAt(i + 1);
					byte val = 0;
					try {
						val = Integer.valueOf(hex, 16).byteValue();
						utf_str.add(val);
					} catch (Exception e) {
					}
					i++;
					in_utf = false;
					continue;
				}
			}
		}
		byte[] utf_str_byte = new byte[utf_str.size()];
		for (int i = 0; i < utf_str.size(); i++) {
			utf_str_byte[i] = utf_str.get(i);
		}
		return utf_str_byte;
	}

	/**
	 * Decodes a quoted printable or base64 encoded string to UTF8
	 * 
	 * @param str
	 *            quoted printable or base64 encoded string
	 * @return UTF-8 string
	 */
	@TargetApi(8)
	public static String decodeText(String str) {
		if (str.contains("=?") && str.contains("?=")) {
			String[] parts = str.substring(str.indexOf("=?")).split("\\?", 5);
			parts[0] = str.substring(0, str.indexOf("=?")) + parts[0];

			if (parts.length != 5)
				return str; // error decoding

			String type = parts[2];
			byte[] utf_str_byte = str.getBytes();
			String to_decode = parts[3];

			// add part before encoded string to resulting string
			List<Byte> utf_str = new ArrayList<Byte>();

			if (parts[0].length() > 1) {
				for (int i = 0; i < parts[0].length() - 1; i++) {
					utf_str.add((byte) parts[0].charAt(i));
				}
			}

			// quoted printable
			if (type.equals("Q")) {
				byte[] decoded = decodeQP(to_decode);

				utf_str_byte = new byte[utf_str.size() + decoded.length];
				for (int i = 0; i < utf_str.size(); i++) {
					utf_str_byte[i] = utf_str.get(i);
				}
				for (int i = 0; i < decoded.length; i++) {
					utf_str_byte[i + utf_str.size()] = decoded[i];
				}

			}
			// base64
			else if (type.equals("B")) {
				byte[] base64_decoded;
				// 2.1 and above use util/Base64, before we use own base64
				// implementation
				try {
					if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.FROYO) {
						base64_decoded = Base64.decode(to_decode,
								Base64.DEFAULT);
					} else {
						base64_decoded = OwnBase64.decode(to_decode);
					}
				} catch (Exception e) {
					// invalid base64, just return the string
					base64_decoded = to_decode.getBytes();
				}

				utf_str_byte = new byte[base64_decoded.length + utf_str.size()];
				for (int i = 0; i < utf_str.size(); i++) {
					utf_str_byte[i] = utf_str.get(i);
				}
				for (int i = 0; i < base64_decoded.length; i++) {
					utf_str_byte[i + utf_str.size()] = base64_decoded[i];
				}
			}

			try {
				return new String(utf_str_byte, parts[1])
						+ decodeText(parts[4].substring(1));
			} catch (UnsupportedEncodingException e) {
				return str;
			}
		} else
			return str;
	}

	/**
	 * Encode an UTF-8 string to UTF-8/Quoted Printable. Used to encode subject
	 * and author of a post.
	 * 
	 * @param str
	 *            string to encode
	 * @return quoted printable encoded string
	 */
	@TargetApi(8)
	public static String encodeText(String str) {
		byte[] utf_str_byte = str.getBytes();

		List<Byte> ret = new ArrayList<Byte>();
		boolean is_encoding = false;
		boolean qp_encode = true;
		if (str.contains("?") || str.contains("="))
			qp_encode = false;

		// encode as quoted printable
		if (qp_encode) {
			byte[] enc_start = "=?UTF-8?Q?".getBytes();

			for (int i = 0; i < utf_str_byte.length; i++) {
				int val = (int) utf_str_byte[i];
				if (val < 0)
					val += 256;
				if (val > 127) {
					// encode it
					// add encoding start if first time
					if (!is_encoding) {
						for (int j = 0; j < enc_start.length; j++) {
							ret.add(enc_start[j]);
						}
					}
					is_encoding = true;

					// encode character
					ret.add((byte) '=');
					String hex = Integer.toHexString(val).toUpperCase();
					ret.add((byte) hex.charAt(0));
					ret.add((byte) hex.charAt(1));
				} else {
					// no need to encode
					if (is_encoding && utf_str_byte[i] == ' ')
						ret.add((byte) '_');
					else
						ret.add(utf_str_byte[i]);
				}
			}

		} else {
			// encode base64
			is_encoding = true;
			byte[] enc_start = "=?UTF-8?B?".getBytes();
			for (int i = 0; i < enc_start.length; i++)
				ret.add(enc_start[i]);

			byte[] base64_encoded;
			try {
				if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.FROYO) {
					base64_encoded = Base64.encode(str.getBytes(),
							Base64.DEFAULT);
				} else {
					base64_encoded = OwnBase64.encode(str.getBytes())
							.getBytes();
				}
			} catch (Exception e) {
				// invalid base64, just return the string
				base64_encoded = str.getBytes();
			}
			for (int i = 0; i < base64_encoded.length; i++)
				ret.add(base64_encoded[i]);
		}

		// convert to string
		if (is_encoding) {
			ret.add((byte) '?');
			ret.add((byte) '=');
		}

		byte[] ret_val = new byte[ret.size()];
		for (int i = 0; i < ret.size(); i++) {
			ret_val[i] = ret.get(i);
		}
		return new String(ret_val);
	}
}
